// --------------------------------------------------------------------------------------------------------------------
// <copyright file="SerializableArray.cs" company="Microsoft Corporation">
// The MIT License (MIT)
// 
// Copyright (c) 2014, Microsoft Corporation
// 
// Permission is hereby granted, free of charge, to any person obtaining a copy
//  of this software and associated documentation files (the "Software"), to deal
//  in the Software without restriction, including without limitation the rights
//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//  copies of the Software, and to permit persons to whom the Software is
//  furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in
//  all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
//  THE SOFTWARE.
// </copyright>
// --------------------------------------------------------------------------------------------------------------------
namespace Microsoft.Robotics.Runtime
{
    using System;
    using System.IO;
    using System.IO.Compression;
    using System.Runtime.InteropServices;
    using System.Runtime.Serialization;

    /// <summary>
    /// Array representation that enables compact serialization by the DataContractSerializer
    /// </summary>
    [DataContract]
    public class SerializableArray
    {
        /// <summary>
        /// The type of simple type stored in the buffer
        /// </summary>
        [DataMember(Name = "T")]
        private TypeCode typeCode;

        /// <summary>
        /// The array of simple type in byte[] format
        /// </summary>
        [DataMember(Name = "D")]
        private byte[] data;

        /// <summary>
        /// The length of the original uncompressed array
        /// </summary>
        [DataMember(Name = "S")]
        private int size;

        /// <summary>
        /// Determines whether the array is compressed or not.
        /// For files, we don't want to compress the stream.
        /// For streaming over network, we want to compress the stream.
        /// </summary>
        [DataMember(Name = "N")]
        private bool noCompression;

        /// <summary>
        /// Initializes a new instance of the SerializableArray class.
        /// </summary>
        /// <param name="array">The array to serialize.</param>
        /// <param name="noCompression">Determines whether to compress the array or not.</param>
        public SerializableArray(Array array, bool noCompression)
        {
            Type arrayType = array.GetType();
            Type elementType = arrayType.GetElementType();
            this.typeCode = Type.GetTypeCode(elementType);
            this.size = array.Length;
            this.noCompression = noCompression;

            unsafe
            {
                switch (this.typeCode)
                {
                    case TypeCode.Byte:
                        this.data = (byte[])array;
                        break;
                    case TypeCode.Boolean:
                        fixed (bool* bytes = &((bool[])array)[0])
                        {
                            this.data = new byte[array.Length * sizeof(bool)];
                            Marshal.Copy(new IntPtr(bytes), this.data, 0, array.Length * sizeof(bool));
                        }

                        break;
                    case TypeCode.Char:
                        fixed (char* bytes = &((char[])array)[0])
                        {
                            this.data = new byte[array.Length * sizeof(char)];
                            Marshal.Copy(new IntPtr(bytes), this.data, 0, array.Length * sizeof(char));
                        }

                        break;
                    case TypeCode.Double:
                        fixed (double* bytes = &((double[])array)[0])
                        {
                            this.data = new byte[array.Length * sizeof(double)];
                            Marshal.Copy(new IntPtr(bytes), this.data, 0, array.Length * sizeof(double));
                        }

                        break;
                    case TypeCode.Int16:
                        fixed (short* bytes = &((short[])array)[0])
                        {
                            this.data = new byte[array.Length * sizeof(short)];
                            Marshal.Copy(new IntPtr(bytes), this.data, 0, array.Length * sizeof(short));
                        }

                        break;
                    case TypeCode.Int32:
                        fixed (int* bytes = &((int[])array)[0])
                        {
                            this.data = new byte[array.Length * sizeof(int)];
                            Marshal.Copy(new IntPtr(bytes), this.data, 0, array.Length * sizeof(int));
                        }

                        break;
                    case TypeCode.Int64:
                        fixed (long* bytes = &((long[])array)[0])
                        {
                            this.data = new byte[array.Length * sizeof(long)];
                            Marshal.Copy(new IntPtr(bytes), this.data, 0, array.Length * sizeof(long));
                        }

                        break;
                    case TypeCode.Single:
                        fixed (float* bytes = &((float[])array)[0])
                        {
                            this.data = new byte[array.Length * sizeof(float)];
                            Marshal.Copy(new IntPtr(bytes), this.data, 0, array.Length * sizeof(float));
                        }

                        break;
                    case TypeCode.UInt16:
                        fixed (ushort* bytes = &((ushort[])array)[0])
                        {
                            this.data = new byte[array.Length * sizeof(ushort)];
                            Marshal.Copy(new IntPtr(bytes), this.data, 0, array.Length * sizeof(ushort));
                        }

                        break;
                    case TypeCode.UInt32:
                        fixed (uint* bytes = &((uint[])array)[0])
                        {
                            this.data = new byte[array.Length * sizeof(uint)];
                            Marshal.Copy(new IntPtr(bytes), this.data, 0, array.Length * sizeof(uint));
                        }

                        break;
                    case TypeCode.UInt64:
                        fixed (ulong* bytes = &((ulong[])array)[0])
                        {
                            this.data = new byte[array.Length * sizeof(ulong)];
                            Marshal.Copy(new IntPtr(bytes), this.data, 0, array.Length * sizeof(ulong));
                        }

                        break;
                }

                this.SerializationTransform();                
            }
        }

        /// <summary>
        /// Deserializes the original array from byte[] format.
        /// </summary>
        /// <returns>The deserialized array.</returns>
        public Array GetTypedArray()
        {
            byte[] bytes;

            unsafe
            {
                switch (this.typeCode)
                {
                    case TypeCode.Byte:
                        bytes = this.DeserializationTransform(sizeof(byte));
                        return bytes;
                    case TypeCode.Boolean:
                        {
                            bytes = this.DeserializationTransform(sizeof(bool));
                            bool[] result = new bool[this.size];
                            fixed (bool* targetBytes = &result[0])
                            {
                                Marshal.Copy(bytes, 0, new IntPtr(targetBytes), bytes.Length);
                            }

                            return result;
                        }

                    case TypeCode.Char:
                        {
                            bytes = this.DeserializationTransform(sizeof(char));
                            char[] result = new char[this.size];
                            fixed (char* targetBytes = &result[0])
                            {
                                Marshal.Copy(bytes, 0, new IntPtr(targetBytes), bytes.Length);
                            }

                            return result;
                        }

                    case TypeCode.Double:
                        {
                            bytes = this.DeserializationTransform(sizeof(double));
                            double[] result = new double[this.size];
                            fixed (double* targetBytes = &result[0])
                            {
                                Marshal.Copy(bytes, 0, new IntPtr(targetBytes), bytes.Length);
                            }

                            return result;
                        }

                    case TypeCode.Int16:
                        {
                            bytes = this.DeserializationTransform(sizeof(short));
                            short[] result = new short[this.size];
                            fixed (short* targetBytes = &result[0])
                            {
                                Marshal.Copy(bytes, 0, new IntPtr(targetBytes), bytes.Length);
                            }

                            return result;
                        }

                    case TypeCode.Int32:
                        {
                            bytes = this.DeserializationTransform(sizeof(int));
                            int[] result = new int[this.size];
                            fixed (int* targetBytes = &result[0])
                            {
                                Marshal.Copy(bytes, 0, new IntPtr(targetBytes), bytes.Length);
                            }

                            return result;
                        }

                    case TypeCode.Int64:
                        {
                            bytes = this.DeserializationTransform(sizeof(long));
                            long[] result = new long[this.size];
                            fixed (long* targetBytes = &result[0])
                            {
                                Marshal.Copy(bytes, 0, new IntPtr(targetBytes), bytes.Length);
                            }

                            return result;
                        }

                    case TypeCode.Single:
                        {
                            bytes = this.DeserializationTransform(sizeof(float));
                            float[] result = new float[this.size];
                            fixed (float* targetBytes = &result[0])
                            {
                                Marshal.Copy(bytes, 0, new IntPtr(targetBytes), bytes.Length);
                            }

                            return result;
                        }

                    case TypeCode.UInt16:
                        {
                            bytes = this.DeserializationTransform(sizeof(ushort));
                            ushort[] result = new ushort[this.size];
                            fixed (ushort* targetBytes = &result[0])
                            {
                                Marshal.Copy(bytes, 0, new IntPtr(targetBytes), bytes.Length);
                            }

                            return result;
                        }

                    case TypeCode.UInt32:
                        {
                            bytes = this.DeserializationTransform(sizeof(uint));
                            uint[] result = new uint[this.size];
                            fixed (uint* targetBytes = &result[0])
                            {
                                Marshal.Copy(bytes, 0, new IntPtr(targetBytes), bytes.Length);
                            }

                            return result;
                        }

                    case TypeCode.UInt64:
                        {
                            bytes = this.DeserializationTransform(sizeof(ulong));
                            ulong[] result = new ulong[this.size];
                            fixed (ulong* targetBytes = &result[0])
                            {
                                Marshal.Copy(bytes, 0, new IntPtr(targetBytes), bytes.Length);
                            }

                            return result;
                        }
                }
            }

            throw new InvalidOperationException(string.Format("{0} is not a supported type code.", this.typeCode));
        }

        /// <summary>
        /// Apply the serialization transform, which compresses the inner bytes or not as per noCompression flag.
        /// </summary>
        private void SerializationTransform()
        {
            if (!this.noCompression)
            {
                this.Compress();
            }
        }

        /// <summary>
        /// Apply the serialization transform, which compresses the array or not as per noCompression flag.
        /// </summary>
        /// <param name="elementSize">The size in bytes of the elements of the array.</param>
        /// <returns>A byte array containing the deserialized data.</returns>
        private byte[] DeserializationTransform(int elementSize)
        {
            if (!this.noCompression)
            {
                return this.Decompress(elementSize);
            }
            else
            {
                return this.data;
            }
        }    

        /// <summary>
        /// Compresses the inner byte[].
        /// </summary>
        private void Compress()
        {        
            // the compressed data is written to the same buffer to avoid another allocation
            using (MemoryStream stream = new MemoryStream(this.data))
            {
                using (DeflateStream compressionStream = new DeflateStream(stream, CompressionLevel.Optimal, true))
                {
                    compressionStream.Write(this.data, 0, this.data.Length);
                }

                // get the resulting compressed data
                Array.Resize(ref this.data, (int)stream.Position);
            }
        }

        /// <summary>
        /// Decompresses the inner byte[].
        /// </summary>
        /// <param name="elementSize">The size in bytes of the elements of the array.</param>
        /// <returns>A byte array containing the decompressed data.</returns>
        private byte[] Decompress(int elementSize)
        {         
            byte[] bytes = new byte[this.size * elementSize];

            // decompress the data
            using (MemoryStream stream = new MemoryStream(this.data))
            {
                using (DeflateStream compressionStream = new DeflateStream(stream, CompressionMode.Decompress))
                {
                    compressionStream.Read(bytes, 0, bytes.Length);
                }

                return bytes;
            }
        }
    }
}
